import argopt
import requests
import json
import os
import re
import sys
import xlrd
import win32api
import win32con

base_types = {
    "void": r"%U8d",
    "VO": r"%U8d",
    "S8": r"%S8d",
    "U8": r"%U8d",
    "char": r"%S8d",
    "signed char": r"%S8d",
    "unsigned char": r"%U8d",
    "short": r"%S16d",
    "signed short": r"%S16d",
    "unsigned short": r"%U16d",
    "S16": r"%S16d",
    "U16": r"%U16d",
    "long long": r"%S64d",
    "long": r"%S32d",
    "signed long": r"%S32d",
    "unsigned long": r"%U32d",
    "S32": r"%S32d",
    "U32": r"%U32d",
    "float": r"%F32d",
    "F32": r"%F32d",
    "U64": r"%U64d",
    "S64": r"%S64d",
    "double": r"%S64d",
    "int": r"%S32d",
    "BO": r"%U8d"
}


def get_print_format(ret_type):
    if ret_type in base_types.keys():
        return base_types[ret_type]
    if ret_type in typedef_list.keys():
            if typedef_list[ret_type] != "":
                ret_type = typedef_list[ret_type]
                if len(ret_type) > 4 and ret_type[:4] == 'enum':
                    ret_type = "S32"
                if ret_type in base_types.keys():
                    return base_types[ret_type]
    return base_types["int"]

dic = []
para_list = []
enum_list = []
struct_list = []
union_list = []
typedef_list = []
arg_info = {}


def get_base_name(name):
    ret_name = ""
    ret_name = name.strip()
    ret_name = ret_name.replace('*', '')
    ret_name = ret_name.replace('&', '')
    if ret_name.find('[') > 0:
        ret_name = ret_name[:ret_name.find('[')]
    if ret_name.find('->') > 0:
        ret_name = ret_name[:ret_name.find('->')]
    if ret_name.find('.') > 0:
        ret_name = ret_name[:ret_name.find('.')]
    name_left = name.replace(ret_name, '').strip()
    if name_left != "" and (name_left[0] == '*' or name_left[0] == '&' or name_left[0] == '.' or name_left[0] == '->'):
        name_left = name_left[1:].strip()
    return ret_name, name_left


def find_type_in_para_list(base_name):
    for para in para_list:
        if base_name == get_base_name(para['name'])[0]:
            return para['type_name']
    if base_name in arg_info.keys():
        return arg_info[base_name]
    return "U8"


def find_type_of_value(e_data, ref_value):
    base_name, name_left = get_base_name(e_data)
    ret_type = find_type_in_para_list(base_name)
    if ret_type == "":
        return base_types['U8']
    '''
    once found base type then return
    '''
    while ret_type not in base_types.keys():
        if ret_type in typedef_list.keys():
            if typedef_list[ret_type] != "":
                ret_type = typedef_list[ret_type]
        for struct in struct_list:
            if ret_type == struct['struct_name']:
                base_name, name_left = get_base_name(name_left)
                if base_name in struct['member_list'].keys():
                    ret_type = struct['member_list'][base_name]
                else:
                    ret_type = "U32"
                break
        for union in union_list:
            if ret_type == union['union_name']:
                base_name, name_left = get_base_name(name_left)
                if base_name in union['member_list'].keys():
                    ret_type = union['member_list'][base_name]
                else:
                    ret_type = "U32"
                break
        if ret_type[:4] == 'enum':
            ret_type = "S32"
            break
    ret_type = base_types[ret_type]

    if len(str(ref_value)) > 2:
        if str(ref_value)[:2] == '0x':
            ret_type = ret_type[:-1] + 'x'
    return ret_type


def read_code_before_execute(book_path):
    try: 
        workbook = xlrd.open_workbook(
            r'' +
            book_path
        )
        sheet = workbook.sheet_by_index(1)
        return sheet.cell(1,1).value
    except:
        return ""


def read_case_list(book_path):
    case_list = {}
    workbook = xlrd.open_workbook(
        r'' +
        book_path
    )
    sheet = workbook.sheet_by_index(0)
    row = 0
    while row < sheet.nrows:
        if sheet.cell(row, 0).value == "Test Cases":
            break
        row += 1
    column = 2
    while column < sheet.ncols:
        para_row = row + 1
        if sheet.cell(row, column).value[0] == '#':
            input_list = {}
            output_list = {}
            return_list = {}
            while para_row < sheet.nrows:
                if sheet.cell(para_row, column).value == "-":
                    para_row += 1
                    continue
                if sheet.cell(para_row, 1).value != "":
                    set_value = None
                    if sheet.cell(para_row, column).ctype == 4:
                        if sheet.cell(para_row, column).value == 0:
                            set_value = "FALSE"
                        else:
                            set_value = "TRUE"
                    elif sheet.cell(para_row, column).ctype == 2 and sheet.cell(
                            para_row, column).value % 1 == 0:
                        set_value = int(sheet.cell(para_row, column).value)
                    else:
                        set_value = sheet.cell(para_row, column).value
                    if sheet.cell(para_row, 0).value[:5] == 'Input':
                        input_list.update(
                            {sheet.cell(para_row, 1).value: set_value})
                    else:
                        # output_list.update(
                        #     {sheet.cell(para_row, 1).value: set_value})
                        if sheet.cell(para_row, 0).value == 'Return':
                            return_list.update(
                                {sheet.cell(para_row, 1).value: set_value})
                        else:
                            output_list.update(
                                {sheet.cell(para_row, 1).value: set_value})

                para_row += 1
            case_list.update({
                sheet.cell(row, column).value: {
                    "input": input_list,
                    "output": output_list,
                    "return": return_list
                }
            })
            column += 1
    if sheet.cell(0, 0).value != "":
        case_list.update({"Call_function_code": sheet.cell(0, 0).value.replace('Call_function_code:','')})
    return case_list


def get_path_module(module_path):
    module_path = module_path.replace('\\', '/')
    folder = module_path[:module_path.rfind('/')]
    module = module_path[module_path.rfind('/')+1:module_path.rfind('.')]
    folder = folder[folder.rfind('/')+1:]
    return folder, module

'''
  (include 也放在里头，对于include"xxx.h"的时候转换为../module/xxx.h) done.
'''


def include_path_fix(code, module_path=""):
    folder, dont_care_module = get_path_module(module_path)
    if code.strip()[:10] == '#include "':
        path = code.strip()[10:]
        if path[:2] != '..':
            code = code.replace(path, "../" + folder + '/' + path)
    return code


def get_include_path(opts):
    ret = []
    for module in opts['modules']:
        ret.append('#include "' + opts['path'] +
                   '_unittest/' + module + '_unittest.h"')
    return ret


if __name__ == "__main__":
    data = [{
        'name': 'selected_function',
        'type': type(list),
        'default': [],
        'comment': 'selected functions to unit test'
    }, {
        'name': 'output_path',
        'type': type(str),
        'default': "",
        'comment': 'output path'
    }, {
        'name': 'testbook_path',
        'type': type(list),
        'default': [],
        'comment': 'testbook paths to unit test'
    }, {
        'name': 'help',
        'type': type(bool),
        'default': False,
        'comment': 'module construct tool'
    }]

    help_text = '''
     unittest_code_generator created by WPz.
       syntax:
             unittest_code_generator.py selected_function=
       example:
             python module_construct.py selected_function="['control_traction_initialize','Traction_Init','Traction_Control_10ms','Traction_Output_10ms']" output_path="C:/linde2020/lindeShare/c_project_static_anlyzer/src/output/output.json" testbook_path="C:/sandBox/kcpc/implement/simulation/workspace/src/control_traction"
     '''

    error, unknown, args, opts = argopt.parse(data, help_text)

    H_code = [
        '/**\n',
        '*  \\file unittest.h\n',
        '*\n',
        '*  \\brief\n',
        '*\n',
        '*  \\author Wu Pengzhan  pengzhan.wu@linde-china.com\n',
        '*\n',
        '*\n',
        '*\n',
        '*/\n',
        '#ifndef UNITTEST_H_INCLUDED\n',
        '#define UNITTEST_H_INCLUDED\n',
        '#include "../cfg/prj.h"\n',
        'void unittest_execute( enum test_mode , U32 time );\n',
        '#endif\n',
        '\n',
        '/***********************************************************************************************************************\n',
        '*                             Copyright 2020 Linde Material Handling. All rights reserved.                             *\n',
        '***********************************************************************************************************************/\n'
    ]

    C_code = [
        '/**\n',
        '*  \\file unittest.c\n',
        '*\n',
        '*  \\brief\n',
        '*\n',
        '*  \\author Wu Pengzhan  pengzhan.wu@linde-china.com\n',
        '*\n',
        '*/\n',
        '#ifdef UNIT_TEST_ENABLE\n',
        '#include "unittest.h"\n',
        '#include "../test/test.h"\n',
        '#include "../assertor/assertor.h"\n',
        '#include "../pal/pal_mem.h"\n',
        '/*================================================[ public functions ]================================================*/\n',
        '\n',
        'void unittest_execute(enum test_mode mode, U32 random_seed)\n',
        '{\n',
        '}\n',
        '#endif\n',
        '/***********************************************************************************************************************\n',
        '*                             Copyright 2020 Linde Material Handling. All rights reserved.                             *\n',
        '***********************************************************************************************************************/\n'
    ]

    if not os.path.exists(opts['output_path']):
        print(opts['output_path']+'not exist!')
        exit(1)
    fp = open(opts['output_path'])
    dic = json.loads(fp.read())

    para_list = dic[0]['para_list']
    enum_list = dic[0]["enum_list"]
    struct_list = dic[0]["struct_list"]
    union_list = dic[0]["union_list"]
    typedef_list = dic[0]["typedef_list"]

    if len(dic) < 1:
        exit(1)
    '''
    module 
    '''
    module_to_test = {}
    module_indx = 0
    for module_dic in dic:
        for funcinfo in module_dic['func_infos']:
            if funcinfo['func_name'] in opts['selected_function']:
                module_to_test.update({module_dic["module_path"]: module_indx})

        module_indx += 1
    print(module_to_test)
    '''
    makefile ignore obj 
    '''
    ignore_obj_list = []
    enable_test_list = []
    '''
    loop module
    '''
    for module_path in module_to_test.keys():
        if not os.path.exists(module_path):
            print(module_path+' not exist!')
            exit(1)
        fp = open(module_path)
        lines = []
        for line in fp:
            lines.append(line)
        folder, module = get_path_module(module_path)
        enable_test_list.append(' -D'+module.upper() + '_UNIT_TEST_ENABLE')
        ignore_obj_list.append(' src/'+ folder + '/' + module + '.o')
        module_h = H_code.copy()
        module_h[1] = module_h[1].replace('unittest', module+'_unittest')
        module_h[13] = module_h[13].replace('unittest', module+'_unittest')
       
        '''
        STUB的函数的生成
        把func_dec_list 中的function 行也都提取出来(decalaration变为dic {dec: param/function} ….是否有其他解决办法 多一个func_dec_list …done)
        跟funcation called list进行遍历对比，有called的，
        加上STUB_ 拷贝到xxx_unittest.h
        而那些直接在.h里定义的function 就不STUB化了。
        '''
        Stub_func_list = []
        for func_dec in dic[module_to_test[module_path]]['func_dec_list']:
            for function in dic[module_to_test[module_path]]['func_infos']:
                '''
                只有 decralation 的function 去做STUB化，用来区分STATIC_INLINE的库函数，如果是STATIC_INLINE的库函数，那么就不做STUB化，因为他的功能都比较简单。
                '''
                if func_dec['dec_name'] in function['called_functions']:
                    type_code = func_dec['line'][:func_dec['line'].find(func_dec['dec_name'] + '(')].strip()
                    if type_code.find('void') < 0 and type_code.find('VO') < 0:
                        Stub_func_list.append(
                            func_dec['line'].replace(
                                'extern',''
                            ).replace(
                                func_dec['dec_name'] + '(', 'STUB_'+ func_dec['dec_name'] + '(').replace(';', '\n{\n    STUB_' + func_dec['dec_name'] + '_cnt++;\n    return STUB_'+  func_dec['dec_name'] + '_ret;\n}') + '\n'
                        )
                        Stub_func_list.append(
                            type_code.replace('extern','') + ' STUB_' + func_dec['dec_name'] + '_ret;\n'
                        )
                    else:
                        Stub_func_list.append(
                            func_dec['line'].replace(
                                'extern',''
                            ).replace(
                                func_dec['dec_name'] + '(', 'STUB_'+ func_dec['dec_name'] + '(').replace(';', '\n{\n    STUB_' + func_dec['dec_name'] + '_cnt++;\n'+'}') + '\n'
                        )

                    Stub_func_list.append(
                        'U8 STUB_' + func_dec['dec_name'] + '_cnt;\n'
                    )
                    break
        if not os.path.exists('../src/' + folder + '_unittest'):
            os.mkdir('../src/' + folder + '_unittest')
        fp = open('../src/' + folder + '_unittest/' +
                  module + "_unittest.h", 'w+')
        for line in module_h:
            fp.write(line)
        fp.close()
        '''
        generate unittest.c 
        '''
        module_c = C_code.copy()
        module_c[1] = module_c[1].replace('unittest', module+'_unittest')
        module_c[8] = module_c[8].replace('UNIT_TEST_ENABLE', module.upper() + '_UNIT_TEST_ENABLE')
        module_c[9] = module_c[9].replace('unittest', module+'_unittest')
        module_c[15] = module_c[15].replace('unittest', module+'_unittest')
        module_c.insert(10,'#include "../'+ folder + '/' + module + '.c"\n')
        '''
        test cases execute.
        '''

        for funcinfo in dic[module_to_test[module_path]]['func_infos']:
            if funcinfo['func_name'] in opts['selected_function']:
                arg_info = funcinfo["args"]
                test_execute_code = [
                    '    test_execute("'+funcinfo['func_name'] +
                    ' unittest", mode, random_seed--)\n',
                    '    {\n',
                    '    }\n'
                ]
                test_book_path = opts['testbook_path'] + \
                    '/'+funcinfo['func_name']+'.xlsx'
                if os.path.exists(test_book_path):
                    '''
                    read excel file
                    '''
                    case_list = read_case_list(test_book_path)
                    case_insert_idx = 2
                    '''
                    argument definition
                    '''
                    if arg_info != {}:
                        for arg in arg_info.keys():
                            test_execute_code.insert(
                            case_insert_idx, '        ' + arg_info[arg] + ' ' + arg +';\n')
                            case_insert_idx += 1
                    '''
                    return value is not void,
                    then definition a return parameter.
                    '''
                    if case_list['#1']['return'] != {}:
                        test_execute_code.insert(
                            case_insert_idx, '        ' + funcinfo['ret_type'] + ' ' + funcinfo['func_name']+'_ret ;\n')
                        case_insert_idx += 1

                    code_before_execute = read_code_before_execute(test_book_path)
                    if code_before_execute != "":
                        print(code_before_execute +'\n')
                        test_execute_code.insert(case_insert_idx, code_before_execute +'\n')
                        case_insert_idx += 1
                    '''
                    STUBBING function install
                    '''
                    stubbing_list = []
                    
                    for case_Nm in case_list.keys():
                        if case_Nm == 'Call_function_code':
                            continue
                        input_dic = case_list[case_Nm]['input']
                        while len(input_dic) > 0:
                            for input_item in input_dic.items():
                                if input_item[0][:5] == 'STUB_':
                                    if input_item[0][5:].rfind('_') > 0:
                                        function_name =  input_item[0][5:][:input_item[0][5:].rfind('_')]
                                        if function_name in funcinfo['called_functions'] and function_name not in stubbing_list:
                                            stubbing_list.append(function_name)
                            break

                    for stubbing_func in stubbing_list:
                        test_execute_code.insert(case_insert_idx , '        INSTALL_STUB('+ stubbing_func + ', STUB_' + stubbing_func + ');\n')
                        case_insert_idx += 1


                    '''
                    write case code
                    '''
                    for case_Nm in case_list.keys():
                        if case_Nm == 'Call_function_code':
                            continue
                        '''
                        input data
                        '''
                        input_dic = case_list[case_Nm]['input']
                        input_idx = case_insert_idx
                        while len(input_dic) > 0:
                            input_item = input_dic.popitem()
                            line_code = str(input_item[0]) + \
                                ' = ' + str(input_item[1]) + ';\n'
                            test_execute_code.insert(
                                input_idx, '        '+line_code)
                            case_insert_idx += 1

                        '''
                        return value
                        '''
                        call_function_code = funcinfo['func_name']+'();'

                        if 'Call_function_code' in case_list.keys():
                            call_function_code = case_list['Call_function_code']
                        if case_list[case_Nm]['return'] == {}:
                            test_execute_code.insert(
                                case_insert_idx, '        ' + call_function_code +'\n')
                            case_insert_idx += 1
                        else:
                            test_execute_code.insert(
                                case_insert_idx, '        ' + funcinfo['func_name']+'_ret = ' + call_function_code +'\n')
                            case_insert_idx += 1
                            return_dic = case_list[case_Nm]['return']
                            while len(return_dic) > 0:
                                return_item = return_dic.popitem()
                                '''
                                test_cell("control_pump_tilt","#1",tilt_target_rpm, 1001,"%S16d");
                                '''
                                line_code = 'test_cell("' + funcinfo['func_name'] + '","' + case_Nm + '",' + str(return_item[0]) + ', ' + str(
                                    return_item[1]) + ', "' + get_print_format(funcinfo['ret_type'])+'");\n'
                                test_execute_code.insert(
                                    case_insert_idx, '        '+line_code)
                                case_insert_idx += 1

                        '''
                        output data
                        '''
                        output_idx = case_insert_idx
                        output_dic = case_list[case_Nm]['output']
                        while len(output_dic) > 0:
                            output_item = output_dic.popitem()
                            line_code = 'test_cell("' + funcinfo['func_name'] + '","' + case_Nm + '",' + str(output_item[0]) + ', ' + str(
                                output_item[1]) + ',"' + find_type_of_value(output_item[0], output_item[1])+'");\n'
                            test_execute_code.insert(
                                output_idx, '        ' + line_code)
                            case_insert_idx += 1
                        
                    '''
                    STUBBING function remove
                    '''
                    for stubbing_func in stubbing_list:
                        test_execute_code.insert(case_insert_idx,'        REMOVE_STUB('+ 'STUB_' + stubbing_func + ');\n')
                else:
                    print('test book no found!')
                    exit(1)
                while test_execute_code != []:
                    module_c.insert(18, test_execute_code.pop())

        '''
        stub function
        '''
        for stub_line in Stub_func_list:
            module_c.insert(15, stub_line)
        generate_path = '../src/' + folder + '_unittest/' + module + "_unittest.c"
        if os.path.exists(generate_path):
            ret = win32api.MessageBox(0, "Test cases for module <" + module+"> is exist!\n1.Press yes to overwrite the whole file.\n2.Press No to overwrite only case code\n3.Press cancel to do nothing",
                                      'Warining', win32con.MB_YESNOCANCEL)
            ''' 
            pressed ok 
            '''
            if ret == 6:
                fp = open(generate_path, 'w+')
                for line in module_c:
                    fp.write(line)
                fp.close()
            elif ret == 7:
                lines = []
                fp = open(generate_path, 'r')
                for line in fp:
                    lines.append(line)
                fp.close()
                rep_indx = 0
                while rep_indx < len(lines):
                    if lines[rep_indx][-39:] == '(enum test_mode mode, U32 random_seed)\n':
                        rep_indx += 2
                        while True:
                            if lines[rep_indx] == '}\n' and lines[rep_indx + 2][:10] == '/*********':
                                break
                            lines.remove(lines[rep_indx])
                        break
                    rep_indx += 1
                insert_indx = 0
                while insert_indx < len(module_c):
                    if module_c[insert_indx][-39:] == '(enum test_mode mode, U32 random_seed)\n':
                        insert_indx += 2
                        insert_cnt = 0
                        while True:
                            if module_c[insert_indx] == '}\n' and module_c[insert_indx + 2][:10] == '/*********':
                                break
                            lines.insert(rep_indx + insert_cnt,
                                         module_c[insert_indx])
                            insert_cnt += 1
                            insert_indx += 1
                        break
                    insert_indx += 1

                fp = open(generate_path, 'w+')
                for line in lines:
                    fp.write(line)
                fp.close()

            else:
                '''
                do nothing
                '''
        else:
            fp = open(generate_path, 'w+')
            for line in module_c:
                fp.write(line)
            fp.close()
    ignore_obj_conf = 'IGNORE_OBJ :='
    for ignore_obj in ignore_obj_list:
        ignore_obj_conf += ignore_obj
    enable_flag_conf = 'ENABLE_TEST_CFG := '
    for enable_flag in enable_test_list:
        enable_flag_conf += enable_flag
    fp = open('../makefile.conf','w')
    fp.write(ignore_obj_conf)    
    fp.write('\n')    
    fp.write(enable_flag_conf)    
    fp.close()
    print('OK')
